class EventEmitter {
    private listeners: Map<string, Map<any, Set<(data?: any) => void>>> = new Map();

    public on(event: string, cb: (data?: any) => void, target?: unknown) {
        if (typeof target === 'undefined') {
            target = this
        }

        if (!this.listeners.has(event)) {
            this.listeners.set(event, new Map())
        }

        const map = this.listeners.get(event)

        if (!map.has(target)) {
            map.set(target, new Set())
        }

        const set = map.get(target)

        set.add(cb)
    }

    public off(event?: string, cb?: (data?: any) => void, target?: unknown) {
        if (!event) {
            this.listeners.clear();
            return;
        }

        if (typeof target === 'undefined') {
            target = this
        }

        if (!this.listeners.has(event)) {
            return
        }

        const map = this.listeners.get(event)

        if (!map.has(target)) {
            return
        }

        const set = map.get(target)

        if (cb) {
            set.delete(cb)
        } else {
            set.clear()
        }
    }

    public offTarget(target?: unknown) {
        if (typeof target === 'undefined') {
            target = this
        }

        this.listeners.forEach(function (map) {
            map.delete(target)
        })
    }

    protected emit(event: string, data?: any) {
        if (!this.listeners.has(event)) {
            return
        }

        const map = this.listeners.get(event)

        map.forEach(function (set, target) {
            set.forEach(function (cb) {
                cb.call(target, data)
            })
        })
    }
}

interface Encode {
    (event: string, data?: any, other?: any): string | ArrayBuffer
}

interface Decode {
    (param: string | ArrayBuffer): { event: string, data?: any }
}

interface opt {
    binaryType?: BinaryType,
    connectTimeOut?: number,
    useAutoReconnect?: boolean,
    autoReconnectDelay?: number,
    useHeartbeat?: boolean,
    heartbeatDelay?: number,
    heartbeatTimeOut?: number,
    encode?: Encode,
    decode?: Decode
}

export default class Socket extends EventEmitter {
    static create(url: string, opt?: opt): Socket {
        return new Socket(url, opt);
    }

    private heartbeatTimer = null;
    private heartbeatTimeOutTimer = null;
    private connectTimeOutTimer = null;
    private autoReconnectTimer = null;

    private _state = WebSocket.CLOSED;
    private socket: WebSocket = null;

    // 网址
    private url = '';
    // 编码
    private binaryType: BinaryType = null;

    // 状态
    public get state() { return this._state; }
    // 连接超时时间
    public connectTimeOut = 2000;
    // 重连等待时间
    public autoReconnectDelay = 2000;
    // 心跳等待时间
    public heartbeatDelay = 2000;
    // 心跳超时时间
    public heartbeatTimeOut = 2000;

    // 是否开启自动重连
    public useAutoReconnect = true;
    // 是否启用心跳
    public useHeartbeat = false;

    // 是否开启自动重连
    public encode: Encode = null;
    // 是否启用心跳
    public decode: Decode = null;

    constructor(url: string, opt?: opt) {
        super();

        this.url = url;

        if (opt) {
            for (const key in opt) {
                this[key] = opt[key];
            }
        }
    }

    protected emit(event: string, data?: any) {
        super.emit('message', { event, data })
        super.emit(event, data)
    }

    public connect(useAutoReconnect?: boolean, useHeartbeat?: boolean) {
        if (typeof useAutoReconnect !== 'undefined') {
            this.useAutoReconnect = !!useAutoReconnect;
        }
        if (typeof useHeartbeat !== 'undefined') {
            this.useHeartbeat = !!useHeartbeat;
        }
        this._state = WebSocket.CONNECTING;
        this.emit("connecting");
        this.createSocket();
    }

    public close(useAutoReconnect?: boolean) {
        if (typeof useAutoReconnect !== 'undefined') {
            this.useAutoReconnect = !!useAutoReconnect;
        }
        this._state = WebSocket.CLOSING;
        this.emit("closing");
        this.socket.close();
    }

    private clearSocket() {
        if (this.socket) {
            this.socket.onopen = null;
            this.socket.onmessage = null;
            this.socket.onerror = null;
            this.socket.onclose = null;
            this.socket.close();
            this.socket = null;

            this.heartbeatTimer && clearTimeout(this.heartbeatTimer);
            this.heartbeatTimer = null;
            this.heartbeatTimeOutTimer && clearTimeout(this.heartbeatTimeOutTimer);
            this.heartbeatTimeOutTimer = null;
            this.connectTimeOutTimer && clearTimeout(this.connectTimeOutTimer);
            this.connectTimeOutTimer = null;
            this.autoReconnectTimer && clearTimeout(this.autoReconnectTimer);
            this.autoReconnectTimer = null;
        }
    }

    private createSocket() {
        this.clearSocket();
        this.socket = new WebSocket(this.url);
        this.socket.onopen = this.onopen.bind(this);
        this.socket.onmessage = this.onmessage.bind(this);
        this.socket.onerror = this.onerror.bind(this);
        this.socket.onclose = this.onclose.bind(this);
        if (this.binaryType) {
            this.socket.binaryType = this.binaryType;
        }

        this.connectTimeOutTimer = setTimeout(() => {
            if (!this.isOpened()) {
                this.onclose();
            }
        }, this.connectTimeOut);
    }

    private reconnect() {
        this._state = WebSocket.CONNECTING;
        this.emit("reconnecting");
        this.createSocket();
    }

    private onopen() {
        this._state = WebSocket.OPEN;
        this.emit("open");

        this.heartbeat();
    }

    private onerror() {
        this._state = WebSocket.CLOSING;
        this.emit("error");
    }

    private onclose() {
        if (this._state === WebSocket.CLOSED) {
            return
        }
        this.clearSocket();

        this._state = WebSocket.CLOSED;
        this.emit("closed");

        // 自动重连
        if (this.useAutoReconnect) {
            this.autoReconnectTimer = setTimeout(() => {
                this.reconnect();
            }, this.autoReconnectDelay);
        }
    }

    private heartbeat() {
        if (this.heartbeatTimeOutTimer) clearTimeout(this.heartbeatTimeOutTimer);
        this.heartbeatTimeOutTimer = null;

        // 启用心跳
        if (this.useHeartbeat) {
            this.heartbeatTimer = setTimeout(() => {
                this.ping();
            }, this.heartbeatDelay);

            this.heartbeatTimeOutTimer = setTimeout(() => {
                this.emit("timeout");
            }, this.heartbeatTimeOut);
        }
    }

    private ping() {
        if (this._state === WebSocket.OPEN) {
            this.socket.send("ping#" + Date.now());
            this.emit("ping");
        }
    }

    private onpong() {
        this.emit("pong");
        this.heartbeat();
    }

    public isValid() {
        return !!this.socket;
    }

    public isOpened() {
        return this._state === WebSocket.OPEN;
    }

    public isClosed() {
        return this._state === WebSocket.CLOSED;
    }

    public isConnecting() {
        return this._state === WebSocket.CONNECTING;
    }

    public isClosing() {
        return this._state === WebSocket.CLOSING;
    }

    private onEncode(event: string, data?: any, other?: any) {

        return JSON.stringify({
            event, data
        })
    }

    private onDecode(param: string) {
        return JSON.parse(param);
    }

    public send(event: string, data?: any, other?: any) {
        this.socket.send(this.onEncode(event, data, other));
    }

    private onmessage(event) {
        if (!event || typeof event.data === 'undefined') {
            return;
        }
        // pong消息
        if (typeof event.data === "string" && event.data.indexOf("pong") === 0) {
            this.onpong();
        } else {
            const data = this.onDecode(event.data);
            this.emit(data.event, data.data);
        }
    }
}